作者:i_dovelemon
来源:CSDN
日期:2014 / 11 4
主题: 递归, Hanoi
引言
在算法的世界里,有递归这样一个强大的工具。很多的算法,我们都可以使用递归来实现,并且非常的简单,容易理解。今天,我们就来讲解一个使用递归解决的问题。著名的汉诺塔问题,就能够使用递归算法来解决。我们先来了解汉诺塔问题是什么:
汉诺塔问题最早由一个法国数学家卢卡斯在1890年发表的,当时的版本是这样的,当64个圆盘被从梵塔上移走的时候,世界末日也就来临了。如果祭司每一分钟移动一个圆盘,请估计一下,移走所有的圆盘需要多少年?(移动规则如下:一次只能移动1个盘子,并且每一次移动都要保证上面的盘子要比下面的盘子小)
下面是汉诺塔问题的图示:
也就是说,我们要在满足上面规则的条件下,将第一个柱子上的盘子移动到第三个柱子上去。
解决之道
这个问题,我们能通过递归的思想来解决。首先我们要明确要想将第一个柱子上的圆盘移动到第三个柱子上去,那么我们就需要借助第二个柱子,将第一个柱子上面的n-1个圆盘(假如一共有n个圆盘)移动到第二个柱子上去。这时,我们就能够将第一个柱子上最大的那个圆盘放到第三个柱子上去了。剩下的工作就是将第二个柱子上的全盘在全部移动到第三个柱子上去。如此递归下去,就能够将全部移动完成。
从上面的文字描述中,我们来归纳总结一共有三个步骤:
1.将第一个柱子上的n-1个圆盘移动到第二个柱子上来
2.将第一个柱子上剩下的唯一的一个圆盘移动到第三个柱子上来
3.将第二个柱子上的圆盘移动到第三个柱子上去
在这三个步骤中,第二步很显然是一个单一的没有递归的步骤,所以是直接操作的步骤。那么对于第一个步骤和第三个步骤又将如何实现了?
我们这样来看,上面的三个步骤是总体上对n个全盘进行移动的方法,通过上面的方法就能够移动成功。那么对于n-1,我们同样也可以使用这样的方法来进行,所不同的是三个柱子之间的位置关系发生了变化。如果说我们用hanoi(n, a, b, c)表示,我们将a上的n个柱子借助b移动到c上去。那么对于n-1的情况,我们同样的使用上面三个步骤来进行,也就是此时我希望将我a柱子上的n-1个圆盘借助于c移动到b柱子上来,因为这时上一次在n的情况下我们要进行的操作的目的,所以此次的操作我们可以用hanoi(n-1,a,c,b)来表示。这样表示是因为,我们使用代换的思想,将三个柱子之间的位置关系调换一下,使得我们借助于c来将a上的圆盘移动到b上去。就是这样不断的调换位置,当我们在次进行n-2的递归的时候,由于位置调换,又变成了hanoi(n-2,a,b,c),如此反复下去。
当然,上面的步骤仅仅是归纳总结里面的第一个步骤的递归情况,也就是说当上面的递归操作完成之后,我们仅仅完成了归纳总结里面关于将n-1个盘子移动到第二个柱子上的操作。同时还要注意,在递归的过程中,我们需要一个条件来使得递归结束,这个结束条件就是当a柱子上只有一个圆盘的时候(注意,这里的a不是指图中固定的第一个柱子,由于在递归的过程中要不断的调换他们的位置,所以a可能是任意一个柱子),我们就可以直接将这个圆盘放在c上(c也同样如a解释),递归操作就此结束了。
好了,当一个步骤完成了之后,这时第一个柱子上只剩下一个圆盘了,我们直接将这个圆盘放在第三个柱子上。此步骤,没有任何递归操作。
最后一个步骤和第一个步骤雷同。只不过调换位置不同而已。此时,我们希望的是将第二个柱子上的n-1个柱子,移动到第三个柱子上去,也就是用hanoi(n-1, b, a, c)。如此就能够完成所有的递归操作了。
代码实现
听着上面的算法,好像挺复杂,但是看代码的话,实际上非常的简单,如下所示:
<span style="font-family:Microsoft YaHei;">void HelloWorld::hanoi(unsigned int n, unsigned int a, unsigned int b, unsigned int c)
{
if(n == 1)
{
MOVE_NODE m ;
m.s = a ;
m.e = c ;
m_MoveMement.push(m);
m_out<< "Move from " << (char)a << " to " << (char)c << endl;
} // end for n == 1
else
{
//move the n-1 from a to b
hanoi(n-1, a, c, b);
//move the a to c
MOVE_NODE m;
m.s = a ;
m.e = c ;
m_MoveMement.push(m);
m_out<< "Move from " << (char)a << " to " << (char)c << endl;
//move the n-1 from b to c
hanoi(n-1, b, a, c);
}
}// end for hanoi</span>
我最初理解这个算法的时候,怎么也想不明白。总是想要自己对n个柱子进行移动试试看。但是发现,很难想出来,所以总是不理解这个过程。后来,我干脆放弃移动,仅仅从算法的层面上来考虑,我要在大脑中想最开始移动哪个到哪个去。然后就能够通过这个递归的思想之后如何解决。并且将移动的步骤打印输出出来,在在这个游戏上
按照输出的步骤进行移动实验,的确可行。也就是说,这个算法描述的是递归调用树的根,如果你想要知道第一步怎么移动的话,需要找到这个递归调用树的第一个子节点,但是对于我们大脑来说,很难深入那么多层,当圆盘的数目增加的时候,更是难以想象。我们来通过算法分析查看下这个算法的时间复杂度如何。
对于这个算法的基本操作应该就是进行移动的操作。而这个操作的次数关系也是递归的,该操作的调用次数递归关系如下:
X(n) = X(n - 1) + 1 + X(n - 1) 其中第一个X(n - 1)表示归纳总结里面第一个步骤所调用基本操作的次数,而中间的1表示当前递归只调用1次基本操作,剩下的一个X(n-1)表示归纳总结里面第三个步骤所调用的基本操作的次数。很显然,这个次数关系实际上也是一个递归关系。我们通过递归推导能够得出最后的公式如下:
X(n) = 2 ^n - 1
也就是说这个算法的复杂度是O(2^n),指数级的算法。虽然这个算法很优雅,但是它的效率却不敢恭维。当圆盘数量达到64的时候,想想就知道要花多少时间才能够移动完毕。
为了掩饰算法,我使用cocos2d-x开发了一个动态算法演示程序,感兴趣的读者可以自行下载:
在这个程序中,有个名为Config.ini的文件,文件中有两个参数,分别是圆盘的个数,以及移动一个圆盘的时间。读者可以自行调节这两个参数。同时,程序会将步骤输出保存在一个名为step.txt的文件中。读者可以根据这个步骤,将上面的游戏玩通关哦!!!